package org.copper.context
{
	import flash.system.System;
	import flash.utils.Dictionary;
	import flash.utils.describeType;
	import flash.utils.getDefinitionByName;
	
	import avmplus.getQualifiedClassName;
	
	import org.copper.utils.ConditionUtil;
	import org.copper.utils.StringUtils;
	
	
	
	
	
	/**
	 * Thin inversion of control layer.
	 */
	public final class ApplicationContext
	{
		//random static string used to make sure no instance of ApplicationContext will be created directly.
		private static const factoryProtection:String = "__factoryProtection" + (Math.random() * (new Date()).time);
		
		//table holding classes by factory method name.
		private var m_declaredClassesByName:Dictionary;
		//table holding classes by class name.
		private var m_declaredClassesByClassName:Dictionary;
		//table holding minimized class descriptors for fast injection.
		private var m_descriptors:Dictionary;
		
		/**
		 * Constructor.
		 * Invoked only by #createContext method.
		 * All the descriptors and dependencies are initialized here.
		 */
		public function ApplicationContext(__factoryProtection:String, __cfg:Array)
		{
			ConditionUtil.assert(__factoryProtection == factoryProtection, "Can not create an instance. Use 'createContext' method.");
			
			m_declaredClassesByName = new Dictionary();
			m_declaredClassesByClassName = new Dictionary();
			m_descriptors = new Dictionary();
			
			//iterating over configuration classes.
			for each(var __config:Object in __cfg)
			{
				const classAsXML:XML = describeType(__config);
				const methodDescriptors:XMLList = classAsXML.method;
				const dlen:int = methodDescriptors.length();
				for(var i:int = 0; i < dlen; i++)
				{
					const descriptor:XML = methodDescriptors[i];
					const params:XMLList = descriptor.parameter;
					const metadata:Dictionary = buildMethadata(descriptor.metadata);
					
					const onCreateObj:Object = metadata["onCreate"];
					const onDestroyObj:Object = metadata["onDestroy"];
					
					const isSingleton:Boolean = metadata["Singleton"] == undefined?false:true;
					const onCreate:String = onCreateObj == null?null:onCreateObj["name"];
					const onDestroy:String = onDestroyObj == null?null:onDestroyObj["name"];
					
					const name:String = StringUtils.copyString(descriptor.@name);
					const returnType:String = StringUtils.copyString(descriptor.@returnType);
					if (m_descriptors[returnType] == undefined)
					{
						m_descriptors[returnType] = buildClassDescriptor(returnType);
					}
					const factory:Function = __config[name];
					
					ConditionUtil.assert(m_declaredClassesByName[name] == undefined, "Class declaration can be defined only once. Duplicate class name: ", name);
					
					const bnd:____bundle = new ____bundle();
					bnd.factory = factory;
					bnd.singelton = isSingleton;
					bnd.onCreate = onCreate;
					bnd.onDestroy = onDestroy;
					
					m_declaredClassesByName[name] = bnd;
					
					const declarationByClassName:____bundle = params.length() == 0?bnd:null;
					if (declarationByClassName != null)
					{
						const definedClassByName:____bundle = m_declaredClassesByClassName[returnType];
						if (definedClassByName == null)
						{
							m_declaredClassesByClassName[returnType] = declarationByClassName;
						}
						else
						{
							definedClassByName.creationByClassEnabled = false;
						}
					}
					
					System.disposeXML(descriptor);
				}
				System.disposeXML(classAsXML);
			}
		}
		
		/**
		 * Returns an instance of object created by specified factory method.
		 */
		public function getObjectByName(name:String, ...args):Object
		{
			const factory:____bundle = m_declaredClassesByName[name];
			ConditionUtil.assert(factory != null, "Instance with id '", name,"' not found...");
			var factoryFunction:Function;
			if (factory.singelton)
			{
				var inst:Object = factory.instance;
				if (inst == null)
				{
					factoryFunction = factory.factory;
					inst = autoInjectByObject(factoryFunction.apply(factoryFunction, args), factory);
					factory.instance = inst;
				}
				
				return inst;
			}
			else
			{
				factoryFunction = factory.factory;
				return autoInjectByObject(factoryFunction.apply(factoryFunction, args), factory);
			}
		}
		
		/**
		 * Destroys this context.
		 */
		public function destroy():void
		{
			for(var key:String in m_declaredClassesByName)
			{
				const bundle:____bundle = m_declaredClassesByName[key];
				if (bundle.instance != null)
				{
					if (bundle.onDestroy != null)
					{
						bundle.instance[bundle.onDestroy]();
					}
				} 
			}
			m_declaredClassesByClassName = null;
			m_declaredClassesByName = null;
			m_descriptors = null;
			System.gc();
		}
		
		public function getObjectByClassName(className:String):Object
		{		
			const factory:____bundle = m_declaredClassesByClassName[className];
			ConditionUtil.assert(factory != null, "Class", className," not found or no declaration with 0 params were found...");
			ConditionUtil.assert(factory.creationByClassEnabled, "There are more than one '", className, "' declarations in the configuration file. Use 'name' parameter in 'autowrite attribute to determine the exact instance.");
			
			if (factory.singelton)
			{
				var inst:Object = factory.instance;
				if (inst == null)
				{
					inst = autoInject(factory.factory(), className, factory);
					factory.instance = inst;
				}
				
				return inst;
			}
			else
				return autoInject(factory.factory(), className, factory);
		}
		
		public static function  createContext(...config):ApplicationContext
		{
			const configResult:Array = [];
			const len:uint = config.length;
			configResult.length = len;
			for(var i:uint = 0; i < len; i++)
			{
				var appConfig:* = config[i];
				if (appConfig is Class)appConfig = new appConfig();
				configResult[i] = appConfig;
			}
			
			return new ApplicationContext(factoryProtection, configResult);
		}
		
		private function autoInject(inst:Object, className:String, bundle:____bundle):Object
		{
			const _me:ApplicationContext = this;
			const descriptor:____classDescriptor = m_descriptors[className];
			var list:Vector.<____entry> = descriptor.autowritedFields;
			var entry:____entry;
			const onCreate:String = bundle.onCreate;
			
			for each(entry in list)
			{
				inst[entry.key] = _me.getObjectByClassName(entry.value);
			}
			
			list = descriptor.nameAutoWritedFields;
			for each(entry in list)
			{
				inst[entry.key] = _me.getObjectByName(entry.value);
			}
			
			if (onCreate != null)
				inst[onCreate]();
			
			return inst;
		}
		
		private function autoInjectByObject(inst:Object, bundle:____bundle):Object
		{
			return autoInject(inst, getQualifiedClassName(inst), bundle);
		}
		
		
		
		private static function buildClassDescriptor(clsName:String):____classDescriptor
		{
			const descriptor:XML = describeType(getDefinitionByName(clsName));
			const result:____classDescriptor = new ____classDescriptor();
			result.name = descriptor.name;
			const flds:Vector.<____entry> = new Vector.<____entry>();
			const namedFlds:Vector.<____entry> = new Vector.<____entry>();
			const fields:XMLList = descriptor.factory.variable;
			const accessors:XMLList = descriptor.factory.accessor;
			var xml:XML;
			var mt:XML;
			var meta:XMLList;
			var args:XMLList;
			var argXml:XML;
			
			for each(xml in fields)
			{
				meta = xml.metadata;
				for each(mt in meta)
				{
					if (StringUtils.copyString(mt.@name) == "autowrite")
					{
						args = mt.arg;
						if (args.length() > 0)
						{
							for each(argXml in args)
							{
								if (StringUtils.copyString(argXml.@key) == "name")
								{
									namedFlds.push(new ____entry(StringUtils.copyString(xml.@name), StringUtils.copyString(argXml.@value)));
								}
								System.disposeXML(argXml);
							}
						}
						else
						{
							flds.push(new ____entry(StringUtils.copyString(xml.@name), StringUtils.copyString(xml.@type)));
						}
						System.disposeXML(mt);
					}
					System.disposeXML(xml);
				}
			}
			
			for each(xml in accessors)
			{
				meta = xml.metadata;
				for each(mt in meta)
				{
					if (StringUtils.copyString(mt.@name) == "autowrite")
					{
						args = mt.arg;
						if (args.length() > 0)
						{
							for each(argXml in args)
							{
								if (StringUtils.copyString(argXml.@key) == "name")
								{
									namedFlds.push(new ____entry(StringUtils.copyString(xml.@name), StringUtils.copyString(argXml.@value)));
								}
								System.disposeXML(argXml);
							}
						}
						else
						{
							flds.push(new ____entry(StringUtils.copyString(xml.@name), StringUtils.copyString(xml.@type)));
						}
					}
					System.disposeXML(mt);
				}
				System.disposeXML(xml);
			}
			
			result.autowritedFields = flds;
			result.nameAutoWritedFields = namedFlds;
			
			System.disposeXML(descriptor);
			return result;
		}
		
		private static function buildMethadata(list:XMLList):Dictionary
		{
			const dict:Dictionary = new Dictionary();
			if (list == null)
				return dict;
			
			for each(var xml:XML in list)
			{
				
				const name:String = StringUtils.copyString(xml.@name);
				const inner:Dictionary = new Dictionary();
				const argList:XMLList = xml.arg;
				for each(var inxml:XML in argList)
				{
					const key:String = StringUtils.copyString(inxml.@key);
					const value:String = StringUtils.copyString(inxml.@value);
					inner[key] = value;
				}
				dict[name] = inner;
				
				System.disposeXML(xml);
			}
			return dict;
		}
	}
}

/**
 * This bundle describs the rule of imstance creation derived from factory function.
 * */
final class ____bundle
{
	public var singelton:Boolean = false;
	public var factory:Function = null;
	public var instance:Object = null;
	public var onCreate:String = null;
	public var onDestroy:String = null;
	public var creationByClassEnabled:Boolean = true;
}

/**
 * Created for each class in the context.
 * */
final class ____classDescriptor
{
	public var name:String;
	public var autowritedFields:Vector.<____entry>;
	public var nameAutoWritedFields:Vector.<____entry>;
}

/**
 * Key - value entity.
 * */
final class ____entry
{
	public function ____entry(Key:String, Value:String)
	{
		key = Key;
		value = Value;
	}
	
	public var key:String;
	public var value:String;
}

